-
Notifications
You must be signed in to change notification settings - Fork 349
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
PacketTunnel: introduce proper state + blocked state #5183
Conversation
8bd34c5
to
f3c7507
Compare
63d0b2a
to
07f362d
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewed 1 of 17 files at r2, 1 of 1 files at r3.
Reviewable status: 2 of 66 files reviewed, 5 unresolved discussions (waiting on @pronebird)
ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift
line 138 at r2 (raw file):
parsedOptions.selectorResult = selectorResult } else { parsedOptions.launchSource = tunnelOptions.isOnDemand() ? .onDemand : .system
This should already be the default (from init), so no need to set it again.
ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift
line 112 at r4 (raw file):
} override func wake() {
Why not keeping to async here?
ios/PacketTunnelCore/Actor/Actor.swift
line 178 at r3 (raw file):
- Parameter date: date when last key rotation took place. */ public func notifyKeyRotated(date: Date? = nil) async {
Nit: This is perhaps more of a "notifyKeyRotation" rather than the key actually have been rotated. If rotation fails (or is attempted), the actor will still be notified with a "last rotation date".
ios/PacketTunnelCore/Actor/Actor.swift
line 196 at r3 (raw file):
case var .connecting(connState): if mutateConnectionState(&connState) { state = .connecting(connState)
Why do we set the same state again?
ios/PacketTunnelCore/Actor/Actor.swift
line 231 at r3 (raw file):
/** Cients should call this method to notify actor when device becomes awake.
Nit: "Clients"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewed 20 of 63 files at r1, 2 of 17 files at r2, all commit messages.
Reviewable status: 24 of 66 files reviewed, 8 unresolved discussions (waiting on @pronebird)
ios/PacketTunnel/PacketTunnelProvider/SettingsReader.swift
line 49 at r4 (raw file):
private extension DNSSettings { /** Converys `DNSSettings` to `SelectedDNSServers` structure.
Nit: "Converts"
ios/PacketTunnelCore/Actor/State.swift
line 81 at r4 (raw file):
/// Error state. /// This state is normally entered when the tunnel is unable to start or reconnect. /// In this state the tunnel blocks all nework connectivity by setting up a peerless WireGuard tunnel, and either awaits user action or, in cirtain
Nit: "Certain"
ios/PacketTunnelCore/Actor/TaskQueue.swift
line 74 at r4 (raw file):
/// Remove task by ID previously returned by call to `registerTask()`. private func unregisterTask(_ taskId: TaskId) { let index = queuedTasks.firstIndex { $0.id == taskId }
Can be simplified: queuedTasks.removeAll { $0.id == taskId }
1b1e5ba
to
bfe97cb
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewable status: 22 of 68 files reviewed, 8 unresolved discussions (waiting on @rablador)
ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift
line 138 at r2 (raw file):
Previously, rablador (Jon Petersson) wrote…
This should already be the default (from init), so no need to set it again.
Different value is being set. This branch infers that if relay selector is not present then it means the system launched the packet tunnel. I refined the else
branch. Please double check.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewable status: 21 of 68 files reviewed, 6 unresolved discussions (waiting on @rablador)
ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift
line 112 at r4 (raw file):
Previously, rablador (Jon Petersson) wrote…
Why not keeping to async here?
wake()
is defined as non-async in the superclass.
ios/PacketTunnel/PacketTunnelProvider/SettingsReader.swift
line 49 at r4 (raw file):
Previously, rablador (Jon Petersson) wrote…
Nit: "Converts"
Done.
ios/PacketTunnelCore/Actor/Actor.swift
line 196 at r3 (raw file):
Previously, rablador (Jon Petersson) wrote…
Why do we set the same state again?
The actor only mutates the associated value (ConnectionState
) in here. It will later call reconnectTunnel()
(see the task created in startKeySwitchTask()
)
ios/PacketTunnelCore/Actor/Actor.swift
line 231 at r3 (raw file):
Previously, rablador (Jon Petersson) wrote…
Nit: "Clients"
Was it fixed?
ios/PacketTunnelCore/Actor/State.swift
line 81 at r4 (raw file):
Previously, rablador (Jon Petersson) wrote…
Nit: "Certain"
Done
ios/PacketTunnelCore/Actor/TaskQueue.swift
line 74 at r4 (raw file):
Previously, rablador (Jon Petersson) wrote…
Can be simplified:
queuedTasks.removeAll { $0.id == taskId }
removeAll()
iterates over all of elements in array that why I prefer firstIndex() + remove(at:)
combo since it's expected to have only one task with the same ID in the queue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewable status: 20 of 68 files reviewed, 10 unresolved discussions (waiting on @pronebird and @rablador)
ios/PacketTunnelCore/Actor/Actor.swift
line 398 at r4 (raw file):
*/ private func tryStart(nextRelay: NextRelay = .random) async throws { func makeConnectionState(settings: Settings) throws -> ConnectionState? {
This is a big enough function to warrant standing on its own, was there a specific reason to inline it like this ?
ios/PacketTunnelCore/Actor/Actor.swift
line 572 at r4 (raw file):
- Parameter reason: reason why the actor is entering error state. */ private func setErrorStateInner(with reason: BlockedStateReason) async {
nit
I don't know if it's worth, but we can use the swift 5.9 "switch as expression" feature to avoid repeating the state change like so
private func setErrorStateInner(with reason: BlockedStateReason) async {
let blockedState: BlockedState? = switch state {
case .initial:
BlockedState(
reason: reason,
relayConstraints: nil,
currentKey: nil,
keyPolicy: .useCurrent,
networkReachability: defaultPathObserver.defaultPath?.networkReachability ?? .undetermined,
recoveryTask: startRecoveryTaskIfNeeded(reason: reason),
priorState: state.priorState!
)
case let .connected(connState), let .connecting(connState), let .reconnecting(connState):
BlockedState(
reason: reason,
relayConstraints: connState.relayConstraints,
currentKey: nil,
keyPolicy: connState.keyPolicy,
networkReachability: connState.networkReachability,
priorState: state.priorState!
)
// Funky / ugly syntax here, but necessary to return a single value that the compiler can type check
case var .error(blockedState): {
if blockedState.reason != reason {
blockedState.reason = reason
}
return blockedState
}()
case .disconnecting, .disconnected:
nil
}
if let blockedState {
state = .error(blockedState)
await configureAdapterForErrorState()
}
}
ios/PacketTunnelCore/Actor/Actor.swift
line 379 at r5 (raw file):
// MARK: - Network Reachability
It would be nice to split this into multiple files if possible
like Actor+Reachability
etc...
ios/PacketTunnelCore/Actor/Actor.swift
line 637 at r5 (raw file):
case .random: return try relaySelector.selectRelay( with: relayConstraints,
Shouldn't this be any relay constraints in case of the .random
selection ? Or are we trying to still respect whatever constraint was chosen by the user here ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewable status: 20 of 68 files reviewed, 10 unresolved discussions (waiting on @buggmagnet and @rablador)
ios/PacketTunnelCore/Actor/Actor.swift
line 398 at r4 (raw file):
Previously, buggmagnet wrote…
This is a big enough function to warrant standing on its own, was there a specific reason to inline it like this ?
No particular reason I think. I'll move it into separate function.
ios/PacketTunnelCore/Actor/Actor.swift
line 572 at r4 (raw file):
Previously, buggmagnet wrote…
nit
I don't know if it's worth, but we can use the swift 5.9 "switch as expression" feature to avoid repeating the state change like soprivate func setErrorStateInner(with reason: BlockedStateReason) async { let blockedState: BlockedState? = switch state { case .initial: BlockedState( reason: reason, relayConstraints: nil, currentKey: nil, keyPolicy: .useCurrent, networkReachability: defaultPathObserver.defaultPath?.networkReachability ?? .undetermined, recoveryTask: startRecoveryTaskIfNeeded(reason: reason), priorState: state.priorState! ) case let .connected(connState), let .connecting(connState), let .reconnecting(connState): BlockedState( reason: reason, relayConstraints: connState.relayConstraints, currentKey: nil, keyPolicy: connState.keyPolicy, networkReachability: connState.networkReachability, priorState: state.priorState! ) // Funky / ugly syntax here, but necessary to return a single value that the compiler can type check case var .error(blockedState): { if blockedState.reason != reason { blockedState.reason = reason } return blockedState }() case .disconnecting, .disconnected: nil } if let blockedState { state = .error(blockedState) await configureAdapterForErrorState() } }
Good idea let me try it.
ios/PacketTunnelCore/Actor/Actor.swift
line 379 at r5 (raw file):
Previously, buggmagnet wrote…
It would be nice to split this into multiple files if possible
likeActor+Reachability
etc...
Ok I wasn't sure but I'll look into doing that.
ios/PacketTunnelCore/Actor/Actor.swift
line 637 at r5 (raw file):
Previously, buggmagnet wrote…
Shouldn't this be any relay constraints in case of the
.random
selection ? Or are we trying to still respect whatever constraint was chosen by the user here ?
The latter. "random" means to pick a random relay satisfying constraints chosen by user. If constraints yield zero relays then we enter blocked state (error).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewed 8 of 63 files at r1, 5 of 17 files at r2, 1 of 14 files at r5.
Reviewable status: 31 of 73 files reviewed, 10 unresolved discussions (waiting on @buggmagnet and @pronebird)
ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift
line 138 at r2 (raw file):
Previously, pronebird (Andrej Mihajlov) wrote…
Different value is being set. This branch infers that if relay selector is not present then it means the system launched the packet tunnel. I refined the
else
branch. Please double check.
Ah, I'm silly, didn't actually read properly...
ios/PacketTunnel/WireGuardAdapter/WgAdapter.swift
line 54 at r4 (raw file):
let dispatchGroup = DispatchGroup() dispatchGroup.enter()
Why do we use a group for this?
ios/PacketTunnelCore/Actor/Actor.swift
line 196 at r3 (raw file):
Previously, pronebird (Andrej Mihajlov) wrote…
The actor only mutates the associated value (
ConnectionState
) in here. It will later callreconnectTunnel()
(see the task created instartKeySwitchTask()
)
Yeah, I saw this during test session with @buggmagnet today. Nevermind, lost myself in code and just missed that param was inout.
ios/PacketTunnelCore/Actor/State+Extensions.swift
line 119 at r4 (raw file):
var shouldRestartAutomatically: Bool { switch self { case .deviceLocked, .outdatedSchema:
@buggmagnet here, typing on my keyboard.
In practice, whenever we return.outdatedSchema
, we need the user to restart the app for the schema update to apply.
This means we will get stuck in a boot loop, endlessly trying to start (and fail) the tunnel. I'm not sure this is the correct approach in this case.
We should probably return false here, and wait for the user to relaunch the app, since we will already be in a blocked state.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewed 1 of 1 files at r6, 1 of 2 files at r7, 1 of 7 files at r8, all commit messages.
Reviewable status: 34 of 73 files reviewed, 8 unresolved discussions (waiting on @buggmagnet and @pronebird)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewable status: 33 of 73 files reviewed, 8 unresolved discussions (waiting on @buggmagnet and @rablador)
ios/PacketTunnel/WireGuardAdapter/WgAdapter.swift
line 54 at r4 (raw file):
Previously, rablador (Jon Petersson) wrote…
Why do we use a group for this?
It's used to block current thread until either the response is received from adapter.getRuntimeConfiguration { .. }
or timeout (1 second). Tunnel Monitor calls this function to obtain WG adapter stats (bytes received/sent) etc..
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewable status: 32 of 73 files reviewed, 8 unresolved discussions (waiting on @buggmagnet and @pronebird)
ios/PacketTunnel/WireGuardAdapter/WgAdapter.swift
line 54 at r4 (raw file):
Previously, pronebird (Andrej Mihajlov) wrote…
It's used to block current thread until either the response is received from
adapter.getRuntimeConfiguration { .. }
or timeout (1 second). Tunnel Monitor calls this function to obtain WG adapter stats (bytes received/sent) etc..
Initially felt strange to use a group for a single value, but I guess it's a convenient to observe both the result and timeout simultaneosly.
487feb8
to
0dc454f
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewed 5 of 19 files at r11.
Reviewable status: 63 of 78 files reviewed, 12 unresolved discussions (waiting on @buggmagnet and @pronebird)
ios/PacketTunnelCore/Actor/CommandChannel.swift
line 28 at r11 (raw file):
Consuming commands can be implemented using a for-await loop. Note that using a loop should also serialize the command handling as the next command will not be consumed until the body of the loop does not complete the iteration.
"does not complete" -> "completes"
ios/PacketTunnelCore/Actor/CommandChannel.swift
line 153 at r11 (raw file):
defer { i -= 1 } assert(i < buffer.count)
Can this ever be false?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewed 2 of 19 files at r11, 4 of 8 files at r12, all commit messages.
Reviewable status: 69 of 78 files reviewed, 10 unresolved discussions (waiting on @pronebird and @rablador)
ios/PacketTunnel/WireGuardAdapter/WireGuardAdapter+Async.swift
line 14 at r11 (raw file):
Previously, pronebird (Andrej Mihajlov) wrote…
This is rather exception, because I trust that
WireGuardAdapter
always calls the completion handler.Checked continuations have an overhead, since they do runtime sanity checks to ensure that continuations aren't released before being resumed.
Unless we measure it, saying "this has overhead" is usually not a good argument, or a good enough reason to use an unsafe API over a safe one.
Please use the checked version instead.
ios/PacketTunnelCore/Actor/Actor.swift
line 92 at r11 (raw file):
Previously, pronebird (Andrej Mihajlov) wrote…
reconnect(to nextRelay: NextRelay, shouldStopTunnelMonitor: Bool)
logs error internally except forTask.sleep()
cancellation which I don't believe should happen currently. Case can be made to make it non-throwing at all.
Considering this is a nonisolated
function that schedules a detached task that we don't keep around, we will not be able to cancel it currently. This means that the inner Task.sleep
also won't get cancelled either.
In that case, please make private func reconnect(to nextRelay: NextRelay, shouldStopTunnelMonitor: Bool)
non throwing
ios/PacketTunnelCore/Actor/Actor.swift
line 272 at r11 (raw file):
Previously, pronebird (Andrej Mihajlov) wrote…
I see the connection. Half of dependencies we inject into
RelaySelectorWrapper
are basically the ones stored inConnectionState
. But how would we select the relay then, since we still need to store newlyselectedRelay
?Something else I have in mind would be to pass
ConnectionState?
intoselectRelay()
so it could pick upcurrentRelay
andconnectionAttemptCount
from there or fill them with blanks when we don't have existing connection state yet. How's that sounds?
Eh let's scratch that for now, I hadn't realized that RelaySelectorWrapper
was defined in PacketTunnel
.
IMHO it shouldn't, and should stay in the RelaySelector
package.
ios/PacketTunnelCore/Actor/Actor+KeyPolicy.swift
line 60 at r12 (raw file):
if let currentKey = blockedState.currentKey { blockedState.lastKeyRotation = lastKeyRotation blockedState.keyPolicy = .usePrior(currentKey, startKeySwitchTask())
I'm not sure I understand the logic here, why do we force a key switch when we are in the blocked state ?
Can you write a comment that explains why ?
ios/PacketTunnelCore/Actor/Actor+KeyPolicy.swift
line 154 at r12 (raw file):
return false case let .usePrior(_, autoCancellingTask):
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewed 13 of 63 files at r1, 11 of 17 files at r2, 10 of 14 files at r5, 9 of 19 files at r11, 4 of 8 files at r12, 3 of 3 files at r13, all commit messages.
Reviewable status: all files reviewed, 25 unresolved discussions (waiting on @pronebird)
ios/PacketTunnelCore/Actor/Actor+SleepCycle.swift
line 17 at r12 (raw file):
`NEPacketTunnelProvider` provides the corresponding lifecycle method. - Important: It's safe to call this method from any thread.
nit
I think we don't need to point out when a method is thread safe.
This should already be implied by the nonisolated
function (since it means it cannot modify the isolated parts in the actor).
If you look at Apple documentation, they usually say something along the lines of "All the methods in XXX are thread safe"
Essentially, we should assume thread safety by default, and specify when something is not thread safe, since documentation is hard to read,
it's better to only point out the important stuff in doc to avoid cognitive load when reading documentation.
ios/PacketTunnelCore/Actor/Command.swift
line 20 at r12 (raw file):
/// Reconnect tunnel. /// `stopTunnelMonitor = false` is only used when tunnel monitor is paused in response to connectivity loss and shouldn't be stopped explciitly,
typo
explciitly
ios/PacketTunnelCore/Actor/Command.swift
line 40 at r12 (raw file):
/// Format command for log output. func logFormat() -> String {
Why not make Command
conform to CustomDebugStringConvertible
instead ?
ios/PacketTunnelCore/Actor/ConfigurationBuilder.swift
line 37 at r12 (raw file):
return TunnelPeer( endpoint: .ipv4(endpoint.ipv4Relay), publicKey: PublicKey(rawValue: endpoint.publicKey)!
We already make this var return TunnelPeer?
So let's not force unwrap here, but return nil if we couldn't construct PublicKey
ios/PacketTunnelCore/Actor/State+Extensions.swift
line 24 at r12 (raw file):
return .reconnecting case .disconnecting, .disconnected, .error: return nil
Since this pretty much should never happen, and if it did, it would be a serious programming mistake, how about we put a fatalError
(with a pretty clear error message) here instead, and make the return type non optional ?
ios/PacketTunnelCore/Actor/State+Extensions.swift
line 56 at r12 (raw file):
// MARK: - Logging func logFormat() -> String {
Same remark about CustomDebugStringConvertible
ios/PacketTunnelCore/Actor/Protocols/RelaySelectorProtocol.swift
line 14 at r12 (raw file):
/// Protocol describing a type that can select a relay. public protocol RelaySelectorProtocol {
This file should really be in RelaySelector
and not in PacketTunnelCore
ios/PacketTunnelCore/TunnelMonitor/TunnelMonitorProtocol.swift
line 22 at r12 (raw file):
nit
When defining a protocol, we don't need to say "protocol that..."
We're already in the context, we know we're dealing with a protocol
We can simply say "A type that can provide tunnel monitoring" here for instance.
There are 5 instances of the phrase "protocol that" at the time of writing this, so not a big deal, but let's not make a habit out of this either.
For example, the Error
protocol starts as such
A type representing an error value that can be thrown.
ios/PacketTunnelCoreTests/ActorTests.swift
line 112 at r12 (raw file):
4. An actor should transition through `.connecting` towards`.connected` state. */ func testLockedDeviceErrorOnBoot() async throws {
This test is currently broken since the introduction of channels. I haven't investigated why, but let's make sure to fix this.
ios/PacketTunnelCoreTests/TaskSleepTests.swift
line 20 at r12 (raw file):
task.cancel() do {
nit
I would have suggested the following simplification
XCTAssertThrowsError(Task { try await task.value }) { error in
XCTAssertTrue(error is CancellationError)
}
BUT that makes the test fail, because suddenly the operation becomes non throwing.
Go figure ¯\_ (ツ)_/¯
ios/PacketTunnelCoreTests/Mocks/MockRelaySelector.swift
line 28 at r12 (raw file):
} extension MockRelaySelector {
We should consolidate all the mocked relays we defined in RelaySelectorTests
in here (if possible)
namely the (very long) private let sampleRelays = REST.ServerRelaysResponse(
variable
ios/PacketTunnelCoreTests/Mocks/MockSettingsReader.swift
line 15 at r12 (raw file):
import class WireGuardKitTypes.PrivateKey struct MockSettingsReader: SettingsReaderProtocol {
nit
This is kind of a big ask, so we don't need to do this here BUT Mocks are not stubs.
Now I'm being super pedantic here, but ideally those should named Stubs probably.
Since we already have expectations built in XCTest
we don't really need mocks in general.
ios/PacketTunnelCoreTests/Mocks/MockTunnelMonitor.swift
line 80 at r12 (raw file):
self?.dispatch(event, after: delay) } simulationBlock(.start, dispatcher)
This should be simulationBlock(command, dispatcher)
ios/PacketTunnelCoreTests/Mocks/MockTunnelMonitor.swift
line 89 at r12 (raw file):
MockTunnelMonitor { command, dispatcher in if case .start = command { dispatcher.send(.connectionEstablished, after: .milliseconds(100))
We can easily go down to 50ms or even 10ms here, gotta go fast ! 🚀
ios/PacketTunnelCoreTests/Mocks/PacketTunnelActor+Mocks.swift
line 15 at r12 (raw file):
static var timingsForTests: PacketTunnelActorTimings { return PacketTunnelActorTimings( bootRecoveryPeriodicity: .milliseconds(100),
Can we lower those to smaller values like 50ms or 10ms ?
ios/PacketTunnelCore/Actor/StartOptions.swift
line 27 at r12 (raw file):
/// Returns a brief description suitable for output to tunnel provider log. public func logFormat() -> String {
Same remark as in another file, let's use CustomDebugStringConvertible
instead
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewable status: 65 of 78 files reviewed, 25 unresolved discussions (waiting on @buggmagnet and @rablador)
ios/PacketTunnel/WireGuardAdapter/WireGuardAdapter+Async.swift
line 14 at r11 (raw file):
Previously, buggmagnet wrote…
Unless we measure it, saying "this has overhead" is usually not a good argument, or a good enough reason to use an unsafe API over a safe one.
Please use the checked version instead.
Done.
ios/PacketTunnelCore/Actor/Actor.swift
line 178 at r3 (raw file):
Previously, pronebird (Andrej Mihajlov) wrote…
I'll address this a bit later once we're through the channel part.
Fixed
ios/PacketTunnelCore/Actor/Actor.swift
line 92 at r11 (raw file):
Previously, buggmagnet wrote…
Considering this is a
nonisolated
function that schedules a detached task that we don't keep around, we will not be able to cancel it currently. This means that the innerTask.sleep
also won't get cancelled either.
In that case, please makeprivate func reconnect(to nextRelay: NextRelay, shouldStopTunnelMonitor: Bool)
non throwing
Fixed.
Yeah, there is a bit of a chicken and egg problem with that task as it calls into "self". Maybe adding explicit call to start handling commands would be better?
ios/PacketTunnelCore/Actor/Actor+KeyPolicy.swift
line 60 at r12 (raw file):
Previously, buggmagnet wrote…
I'm not sure I understand the logic here, why do we force a key switch when we are in the blocked state ?
Can you write a comment that explains why ?
Because key rotation may still happen while in blocked state and because key policy is preserved between states.
However:
- The tunnel should not be reconnected automatically when in blocked state at the time of transition from
.usePrior
to.useCurrent
. - We should consume
currentKey
when moving it into.usePrior
key policy to prevent it from being reused later.
I have addressed that in a separate commit.
ios/PacketTunnelCore/Actor/Actor+KeyPolicy.swift
line 154 at r12 (raw file):
Previously, buggmagnet wrote…
⚠️ Immutable value 'autoCancellingTask' was never used; consider replacing with '_' or removing it
Done.
ios/PacketTunnelCore/Actor/Actor+SleepCycle.swift
line 17 at r12 (raw file):
Previously, buggmagnet wrote…
nit
I think we don't need to point out when a method is thread safe.
This should already be implied by thenonisolated
function (since it means it cannot modify the isolated parts in the actor).
If you look at Apple documentation, they usually say something along the lines of "All the methods in XXX are thread safe"Essentially, we should assume thread safety by default, and specify when something is not thread safe, since documentation is hard to read,
it's better to only point out the important stuff in doc to avoid cognitive load when reading documentation.
Deep. Fixed.
ios/PacketTunnelCore/Actor/Command.swift
line 40 at r12 (raw file):
Previously, buggmagnet wrote…
Why not make
Command
conform toCustomDebugStringConvertible
instead ?
Because we don't use debugPrint()
. logFormat()
is used specifically for log output that we receive with problem reports.
ios/PacketTunnelCore/Actor/CommandChannel.swift
line 28 at r11 (raw file):
Previously, rablador (Jon Petersson) wrote…
"does not complete" -> "completes"
Done.
ios/PacketTunnelCore/Actor/CommandChannel.swift
line 153 at r11 (raw file):
Previously, rablador (Jon Petersson) wrote…
Can this ever be false?
Only if coalescing algo within the loop does not properly adjust i
.
ios/PacketTunnelCore/Actor/ConfigurationBuilder.swift
line 37 at r12 (raw file):
Previously, buggmagnet wrote…
We already make this var return
TunnelPeer?
So let's not force unwrap here, but return nil if we couldn't constructPublicKey
@rablador raised and fixed this today, perhaps he could push his changes on top.
In short if we cannot construct PublicKey()
from raw data, then we should enter blocked state. Returning nil
here would configure empty WG tunnel which is not the intention when endpoint
is provided.
This is extreme edge case as that means that our relay list contains malformed public keys. In conversation with @rablador we figured that throwing an error from here would be better as it would bubble up to tryStart()
and cause actor to enter error state.
ios/PacketTunnelCore/Actor/State+Extensions.swift
line 24 at r12 (raw file):
Previously, buggmagnet wrote…
Since this pretty much should never happen, and if it did, it would be a serious programming mistake, how about we put a
fatalError
(with a pretty clear error message) here instead, and make the return type non optional ?
True but I am not very fond of fatal errors esp. in the tunnel. Since this is only used in a single function within Actor+ErrorState, I figured that we could solve it with a helper function and pass the priorState manually from within switch case.
ios/PacketTunnelCore/Actor/Protocols/RelaySelectorProtocol.swift
line 14 at r12 (raw file):
Previously, buggmagnet wrote…
This file should really be in
RelaySelector
and not inPacketTunnelCore
It's a direct dependency of PacketTunnelActor
and implemented in PacketTunnel
by RelaySelectorWrapper
. RelaySelector
could live without this protocol, actor cannot. So I think we should keep it where it's being used/consumed.
ios/PacketTunnelCoreTests/ActorTests.swift
line 112 at r12 (raw file):
Previously, buggmagnet wrote…
This test is currently broken since the introduction of channels. I haven't investigated why, but let's make sure to fix this.
Some of the changes I have introduced lately mutate state in a few steps vs. one which causes more updates than anticipated before. Connecting state fires twice and the corresponding expectation fails because it only expects to be fulfilled once. So I think I will have to look how I register state transitions using expectations.
ios/PacketTunnelCoreTests/TaskSleepTests.swift
line 20 at r12 (raw file):
Previously, buggmagnet wrote…
nit
I would have suggested the following simplificationXCTAssertThrowsError(Task { try await task.value }) { error in XCTAssertTrue(error is CancellationError) }BUT that makes the test fail, because suddenly the operation becomes non throwing.
Go figure ¯\_ (ツ)_/¯
XCTAssert family of functions accept expressions with @autoclosure
which is defined as non-async function so not much we can do about that unless we roll our own using continuations and wrap everything in custom helpers I guess.
ios/PacketTunnelCoreTests/Mocks/MockRelaySelector.swift
line 28 at r12 (raw file):
Previously, buggmagnet wrote…
We should consolidate all the mocked relays we defined in
RelaySelectorTests
in here (if possible)
namely the (very long)private let sampleRelays = REST.ServerRelaysResponse(
variable
Maybe we could put them in JSON instead.
ios/PacketTunnelCoreTests/Mocks/MockTunnelMonitor.swift
line 80 at r12 (raw file):
Previously, buggmagnet wrote…
This should be
simulationBlock(command, dispatcher)
Done.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewed 4 of 13 files at r14, all commit messages.
Reviewable status: 69 of 78 files reviewed, 19 unresolved discussions (waiting on @pronebird and @rablador)
ios/PacketTunnelCore/Actor/Actor+KeyPolicy.swift
line 60 at r12 (raw file):
Previously, pronebird (Andrej Mihajlov) wrote…
Because key rotation may still happen while in blocked state and because key policy is preserved between states.
However:
- The tunnel should not be reconnected automatically when in blocked state at the time of transition from
.usePrior
to.useCurrent
.- We should consume
currentKey
when moving it into.usePrior
key policy to prevent it from being reused later.I have addressed that in a separate commit.
But if we're in a blocked state, we will be blocking all traffic, as done so in configureAdapterForErrorState
// Configure tunnel with empty WireGuard configuration that consumes all traffic on device
// emulating a firewall blocking all traffic.
private func configureAdapterForErrorState() async {
do {
let configurationBuilder = ConfigurationBuilder(
privateKey: PrivateKey(),
interfaceAddresses: []
)
try await tunnelAdapter.start(configuration: configurationBuilder.makeConfiguration())
} catch {
logger.error(error: error, message: "Unable to configure the tunnel for error state.")
}
}
So key rotations shouldn't work in the first place. Or am I misunderstanding something ?
Or does this mean key rotation operations will leak outside of the tunnel ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewable status: 69 of 78 files reviewed, 15 unresolved discussions (waiting on @pronebird and @rablador)
ios/PacketTunnelCore/Actor/Command.swift
line 20 at r12 (raw file):
Previously, buggmagnet wrote…
typo
explciitly
typo
explcitly
🙃
ios/PacketTunnelCore/Actor/Command.swift
line 40 at r12 (raw file):
Previously, pronebird (Andrej Mihajlov) wrote…
Because we don't use
debugPrint()
.logFormat()
is used specifically for log output that we receive with problem reports.
That's doing exactly the same thing with CustomStringConvertible
though
struct Point: CustomStringConvertible {
var description: String { "x:\(x) y:\(y)" }
let x: Int
let y: Int
func logFormat() -> String {
"x:\(x) y:\(y)"
}
}
let p = Point(x: 1, y: 2)
print(p) // x:1 y:2
print(p.logFormat()) // x:1 y:2
Not a big deal I guess, but not really necessary to complicate our lives that way either.
ios/PacketTunnelCore/Actor/Protocols/RelaySelectorProtocol.swift
line 14 at r12 (raw file):
Previously, pronebird (Andrej Mihajlov) wrote…
It's a direct dependency of
PacketTunnelActor
and implemented inPacketTunnel
byRelaySelectorWrapper
.RelaySelector
could live without this protocol, actor cannot. So I think we should keep it where it's being used/consumed.
The problem with that approach is that this creates an implicit chain dependency between PacketTunnelCore
andRelaySelector
.
Let's say package XYZ is consuming PacketTunnelCore
which declares this protocol, now it also has to consume RelaySelector
even if it doesn't need it.
In our case, it's not a big deal since RelaySelector
is pretty much everywhere, but otherwise, I would say that this breaks the Interface Segregation Principle (No code should depend on interfaces it does not use)
Not going to be blocking on that, but I don't encourage this either.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewable status: 69 of 78 files reviewed, 15 unresolved discussions (waiting on @pronebird and @rablador)
ios/PacketTunnelCore/Actor/Actor+KeyPolicy.swift
line 60 at r12 (raw file):
Previously, buggmagnet wrote…
But if we're in a blocked state, we will be blocking all traffic, as done so in
configureAdapterForErrorState
// Configure tunnel with empty WireGuard configuration that consumes all traffic on device // emulating a firewall blocking all traffic. private func configureAdapterForErrorState() async { do { let configurationBuilder = ConfigurationBuilder( privateKey: PrivateKey(), interfaceAddresses: [] ) try await tunnelAdapter.start(configuration: configurationBuilder.makeConfiguration()) } catch { logger.error(error: error, message: "Unable to configure the tunnel for error state.") } }So key rotations shouldn't work in the first place. Or am I misunderstanding something ?
Or does this mean key rotation operations will leak outside of the tunnel ?
Nevermind, I had forgotten that the packet tunnel process itself will still have internet, and so technically, we can still try key rotation operations in a blocked state.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewable status: 69 of 78 files reviewed, 15 unresolved discussions (waiting on @buggmagnet and @rablador)
ios/PacketTunnelCore/Actor/Actor+KeyPolicy.swift
line 60 at r12 (raw file):
Previously, buggmagnet wrote…
Nevermind, I had forgotten that the packet tunnel process itself will still have internet, and so technically, we can still try key rotation operations in a blocked state.
That's correct. We may still rotate the key bypassing VPN.
ios/PacketTunnelCore/Actor/Protocols/RelaySelectorProtocol.swift
line 14 at r12 (raw file):
Let's say package XYZ is consuming
PacketTunnelCore
which declares this protocol, now it also has to consumeRelaySelector
even if it doesn't need it.
It has to be consumed either way because RelaySelectorResult
is defined in RelaySelector
.
In our case, it's not a big deal since
RelaySelector
is pretty much everywhere, but otherwise, I would say that this breaks the c (No code should depend on interfaces it does not use)
Technically this can fixed. I can have a look at how the output requirement can be formalized.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewable status: 69 of 78 files reviewed, 15 unresolved discussions (waiting on @buggmagnet and @rablador)
ios/PacketTunnelCore/Actor/Command.swift
line 40 at r12 (raw file):
Previously, buggmagnet wrote…
That's doing exactly the same thing with
CustomStringConvertible
thoughstruct Point: CustomStringConvertible { var description: String { "x:\(x) y:\(y)" } let x: Int let y: Int func logFormat() -> String { "x:\(x) y:\(y)" } } let p = Point(x: 1, y: 2) print(p) // x:1 y:2 print(p.logFormat()) // x:1 y:2Not a big deal I guess, but not really necessary to complicate our lives that way either.
I did it exactly this way a few years back, and then decided in favor of explicit function to format a struct specifically for logging because it's explicit in intent and it doesn't get in a way when debugging.
For instance Date.logFormatDate()
specifically formats date suitable for logging and not for output into Xcode console when debugging or in any other context.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewable status: 62 of 79 files reviewed, 13 unresolved discussions (waiting on @buggmagnet and @rablador)
ios/PacketTunnelCore/Actor/Command.swift
line 20 at r12 (raw file):
Previously, buggmagnet wrote…
typo
explcitly
🙃
Done.
ios/PacketTunnelCore/TunnelMonitor/TunnelMonitorProtocol.swift
line 22 at r12 (raw file):
Previously, buggmagnet wrote…
nit
When defining a protocol, we don't need to say "protocol that..."
We're already in the context, we know we're dealing with a protocolWe can simply say "A type that can provide tunnel monitoring" here for instance.
There are 5 instances of the phrase "protocol that" at the time of writing this, so not a big deal, but let's not make a habit out of this either.
For example, the
Error
protocol starts as suchA type representing an error value that can be thrown.
Done.
ios/PacketTunnelCoreTests/ActorTests.swift
line 112 at r12 (raw file):
Previously, pronebird (Andrej Mihajlov) wrote…
Some of the changes I have introduced lately mutate state in a few steps vs. one which causes more updates than anticipated before. Connecting state fires twice and the corresponding expectation fails because it only expects to be fulfilled once. So I think I will have to look how I register state transitions using expectations.
This test will be re-introduced in the follow up work by you and Jon, as agreed today during the call.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewable status: 54 of 83 files reviewed, 13 unresolved discussions (waiting on @buggmagnet and @rablador)
ios/PacketTunnelCore/Actor/Protocols/RelaySelectorProtocol.swift
line 14 at r12 (raw file):
Previously, pronebird (Andrej Mihajlov) wrote…
Let's say package XYZ is consuming
PacketTunnelCore
which declares this protocol, now it also has to consumeRelaySelector
even if it doesn't need it.It has to be consumed either way because
RelaySelectorResult
is defined inRelaySelector
.In our case, it's not a big deal since
RelaySelector
is pretty much everywhere, but otherwise, I would say that this breaks the c (No code should depend on interfaces it does not use)Technically this can fixed. I can have a look at how the output requirement can be formalized.
I have prepared a quick draft commit in 3742490 that replaces the use of RelaySelectorResult
with SelectedRelay
type exported from PacketTunnelCore in order to remove dependency on RelaySelector
. Also it makes it possible to reconnect the tunnel using different options, i.e by passing NextRelay
enum type instead of RelaySelectorResult
as it used to be. I believe this can be useful.
ios/PacketTunnelCoreTests/Mocks/PacketTunnelActor+Mocks.swift
line 15 at r12 (raw file):
Previously, buggmagnet wrote…
Can we lower those to smaller values like 50ms or 10ms ?
Done.
ios/PacketTunnelCoreTests/Mocks/MockSettingsReader.swift
line 15 at r12 (raw file):
Previously, buggmagnet wrote…
nit
This is kind of a big ask, so we don't need to do this here BUT Mocks are not stubs.Now I'm being super pedantic here, but ideally those should named Stubs probably.
Since we already have expectations built in
XCTest
we don't really need mocks in general.
As I understood it applies to all test objects. Renamed the types based on proposed nomenclature. Also put Mock/Stub/Fake at end of type name and not in front as it used to be.
ios/PacketTunnelCoreTests/Mocks/MockTunnelMonitor.swift
line 89 at r12 (raw file):
Previously, buggmagnet wrote…
We can easily go down to 50ms or even 10ms here, gotta go fast ! 🚀
Done.
1a4eac8
to
255fae4
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewed 5 of 13 files at r14, 7 of 8 files at r15, 2 of 2 files at r16, 4 of 13 files at r17, 9 of 9 files at r18, all commit messages.
Reviewable status: 81 of 83 files reviewed, 7 unresolved discussions (waiting on @pronebird and @rablador)
ios/PacketTunnelCore/Actor/Protocols/RelaySelectorProtocol.swift
line 14 at r12 (raw file):
Previously, pronebird (Andrej Mihajlov) wrote…
I have prepared a quick draft commit in 3742490 that replaces the use of
RelaySelectorResult
withSelectedRelay
type exported from PacketTunnelCore in order to remove dependency onRelaySelector
. Also it makes it possible to reconnect the tunnel using different options, i.e by passingNextRelay
enum type instead ofRelaySelectorResult
as it used to be. I believe this can be useful.
Thanks, it's very appreciated ! If we can merge it after merging this PR, that will be nice.
I will look at it soon
ios/PacketTunnelCoreTests/Mocks/MockRelaySelector.swift
line 28 at r12 (raw file):
Previously, pronebird (Andrej Mihajlov) wrote…
Maybe we could put them in JSON instead.
I moved it around to get it globally for tests (not super proud) since I needed it somewhere else for the TunnelManager
tests, but probably in the future we could (should ?) do that. I'll think about it.
ios/PacketTunnelCoreTests/Mocks/MockSettingsReader.swift
line 15 at r12 (raw file):
Previously, pronebird (Andrej Mihajlov) wrote…
As I understood it applies to all test objects. Renamed the types based on proposed nomenclature. Also put Mock/Stub/Fake at end of type name and not in front as it used to be.
Really nice, thanks !
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewable status: 81 of 83 files reviewed, 5 unresolved discussions (waiting on @buggmagnet and @rablador)
ios/PacketTunnelCore/Actor/Protocols/RelaySelectorProtocol.swift
line 14 at r12 (raw file):
Previously, buggmagnet wrote…
Thanks, it's very appreciated ! If we can merge it after merging this PR, that will be nice.
I will look at it soon
Agree. Let's do it after this PR.
ios/PacketTunnelCoreTests/Mocks/MockRelaySelector.swift
line 28 at r12 (raw file):
Previously, buggmagnet wrote…
I moved it around to get it globally for tests (not super proud) since I needed it somewhere else for the
TunnelManager
tests, but probably in the future we could (should ?) do that. I'll think about it.
Cool. Then let's not touch anything in this PR and address it later.
ea10a3b
to
e5f95b5
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewable status: 73 of 87 files reviewed, 5 unresolved discussions (waiting on @buggmagnet and @rablador)
ios/PacketTunnelCore/Actor/State+Extensions.swift
line 56 at r12 (raw file):
Previously, buggmagnet wrote…
Same remark about
CustomDebugStringConvertible
I left the answer on one of similar remarks. So I assume this is just a left over.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewed 4 of 19 files at r11, 1 of 8 files at r12, 2 of 3 files at r13, 9 of 14 files at r14, 8 of 8 files at r15, 2 of 2 files at r16, 4 of 17 files at r17, 7 of 9 files at r18, 2 of 2 files at r19, 2 of 2 files at r20, 1 of 1 files at r21, 8 of 8 files at r22, all commit messages.
Reviewable status: all files reviewed, 7 unresolved discussions (waiting on @buggmagnet and @pronebird)
ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift
line 136 at r22 (raw file):
if !allIPAddresses.isEmpty, !isLoggedSameIP { isLoggedSameIP = true logIfDeviceHasSameIP(than: allIPAddresses)
than -> as
ios/PacketTunnelCore/Actor/Actor+ConnectionMonitoring.swift
line 72 at r19 (raw file):
// Tunnel monitor should already be paused at this point so don't stop it to avoid a reset of its internal // counters. commandChannel.send(.reconnect(.random, stopTunnelMonitor: false))
There was some talk about adding something like a ReconnectionReason
to reconnect command to remove the need for separately mutating state when incerementing the attempt count. We didn't really "finish" the discussion, but I guess the proposed change was put on hold then?
ios/PacketTunnelCore/Actor/Actor+KeyPolicy.swift
line 95 at r19 (raw file):
func switchToCurrentKey() { if switchToCurrentKeyInner() { commandChannel.send(.reconnect(.random))
Why not reconnect to .current? There are more instances in the code base where .random is selected.
ios/PacketTunnelCore/Actor/ConfigurationBuilder.swift
line 37 at r12 (raw file):
Previously, pronebird (Andrej Mihajlov) wrote…
@rablador raised and fixed this today, perhaps he could push his changes on top.
In short if we cannot construct
PublicKey()
from raw data, then we should enter blocked state. Returningnil
here would configure empty WG tunnel which is not the intention whenendpoint
is provided.This is extreme edge case as that means that our relay list contains malformed public keys. In conversation with @rablador we figured that throwing an error from here would be better as it would bubble up to
tryStart()
and cause actor to enter error state.
This has been adressed a separate PR that branches off another branch (and ultimately this one). I think it's fine if we merge it after this PR has been merged.
ios/PacketTunnelCoreTests/Mocks/PacketTunnelActor+Mocks.swift
line 25 at r19 (raw file):
tunnelAdapter: TunnelAdapterProtocol = TunnelAdapterDummy(), tunnelMonitor: TunnelMonitorProtocol = TunnelMonitorStub.nonFallible(), defaultPathObserver: DefaultPathObserverProtocol = DefaultPathObserverFake(),
"Dummy"...?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewable status: all files reviewed, 7 unresolved discussions (waiting on @buggmagnet and @rablador)
ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift
line 136 at r22 (raw file):
Previously, rablador (Jon Petersson) wrote…
than -> as
This was introduced in cae5cdf#diff-ec10e6e167d767e71eba632daeb5dc0012e34846fa7d0d68051cf310f2533afdR242-R251 and I simply reintegrated the changes.
ios/PacketTunnelCore/Actor/Actor+ConnectionMonitoring.swift
line 72 at r19 (raw file):
Previously, rablador (Jon Petersson) wrote…
There was some talk about adding something like a
ReconnectionReason
to reconnect command to remove the need for separately mutating state when incerementing the attempt count. We didn't really "finish" the discussion, but I guess the proposed change was put on hold then?
I don't think it was discussed outside of our private conversation. This can be addressed separately if you feel so. I think that I poked around a bit and the change wasn't as straightforward as I thought but I could revisit that of course.
ios/PacketTunnelCore/Actor/Actor+KeyPolicy.swift
line 95 at r19 (raw file):
Previously, rablador (Jon Petersson) wrote…
Why not reconnect to .current? There are more instances in the code base where .random is selected.
I believe I was told to use random to prevent any kind of potential for tracing a connection between the same client using old and new keys.
ios/PacketTunnelCoreTests/Mocks/PacketTunnelActor+Mocks.swift
line 25 at r19 (raw file):
Previously, rablador (Jon Petersson) wrote…
"Dummy"...?
Path observer uses a memory storage to simulate path observer, so t's not a dummy but fake. @buggmagnet shared a link to the article which breaks down the differences. Did I get it wrong?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewable status: all files reviewed, 6 unresolved discussions (waiting on @buggmagnet and @pronebird)
ios/PacketTunnelCore/Actor/Actor+ConnectionMonitoring.swift
line 72 at r19 (raw file):
Previously, pronebird (Andrej Mihajlov) wrote…
I don't think it was discussed outside of our private conversation. This can be addressed separately if you feel so. I think that I poked around a bit and the change wasn't as straightforward as I thought but I could revisit that of course.
No, it was just between us. And no, let's leave it for now if it means extra work.
ios/PacketTunnelCore/Actor/Actor+KeyPolicy.swift
line 95 at r19 (raw file):
Previously, pronebird (Andrej Mihajlov) wrote…
I believe I was told to use random to prevent any kind of potential for tracing a connection between the same client using old and new keys.
Aha, didn't know that. Ok, that's fine then since we'll still be adhering to the relay constraints.
ios/PacketTunnelCoreTests/Mocks/PacketTunnelActor+Mocks.swift
line 25 at r19 (raw file):
Previously, pronebird (Andrej Mihajlov) wrote…
Path observer uses a memory storage to simulate path observer, so t's not a dummy but fake. @buggmagnet shared a link to the article which breaks down the differences. Did I get it wrong?
Probably not, just that it stood out as something we really don't use in the app elsewhere. Nevermind :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewed 1 of 8 files at r15, 2 of 2 files at r19, 2 of 2 files at r20, 1 of 1 files at r21, 8 of 8 files at r22, all commit messages.
Reviewable status: all files reviewed, 5 unresolved discussions (waiting on @pronebird and @rablador)
ios/PacketTunnelCoreTests/Mocks/PacketTunnelActor+Mocks.swift
line 25 at r19 (raw file):
Previously, rablador (Jon Petersson) wrote…
Probably not, just that it stood out as something we really don't use in the app elsewhere. Nevermind :)
@rablador here's the link I posted : mocks are not stubs
In most of our test cases, we use stubs (an object that returns pre baked responses)
I'm being nitpicky here and ultimately I think we'll be fine with either dummies or stubs in most of our tests.
It's just that I want to make sure we use the correct terminology so that new comers, or people who read our codebase don't get the wrong idea.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewable status: all files reviewed, 3 unresolved discussions (waiting on @buggmagnet and @pronebird)
ios/PacketTunnelCoreTests/Mocks/PacketTunnelActor+Mocks.swift
line 25 at r19 (raw file):
Previously, buggmagnet wrote…
@rablador here's the link I posted : mocks are not stubs
In most of our test cases, we use stubs (an object that returns pre baked responses)I'm being nitpicky here and ultimately I think we'll be fine with either dummies or stubs in most of our tests.
It's just that I want to make sure we use the correct terminology so that new comers, or people who read our codebase don't get the wrong idea.
Got it. According to the article, "Fake" seems correct here.
e5f95b5
to
f29c4f8
Compare
Abstract
Current implementation of packet tunnel isn't designed to account for complexity that we face today.
All of the previous additions to the packet tunnel were accummulative resulting in a bloated state and degraded separation of concern.
It became apparent to me that something had to be done to improve the situation.
My work is mainly focused around the following areas:
Provider
Packet tunnel provider was revamped and a lot of what it used to do is now a part of its dependencies, such as:
PacketTunnelActor
- tunnel state machine implemented as Swift actor (referenced asActor
for short)AppMessageHandler
- responsible for handling IPC messagesDeviceChecker
- service that performs device checksActor
The packet tunnel actor (
Actor
) is responsible for maintaining and monitoring the tunnel connection and network conditions and reducing all that information into consistent state.The actor state is represented by the
State
enum type with distinct variants that describe each phase in the packet tunnel lifecycle:General lifecycle ♻️
Packet tunnel always begins in
.initial
state and ends.disconnected
state over time. Packet tunnel process is not recycled and hence once the.disconnected
state is reached, the process is terminated. The new process is started next time VPN is activated.Reconnecting state
.reconnecting
state can be entered from.connected
state.Error state 🚫
Packet tunnel enters error state in the event of unrecoverable error, that normally requires user interaction.
One exception to that is when network extension is started on boot and does not have access to Keychain or filesystem until device is unlocked. In that case actor enters blocked state but also starts a repeating task that attempts to start the tunnel every 10 seconds until it succeeds.
.error
state can be entered from nearly any other state except when the tunnel is at or past.disconnecting
phase.Exiting error state
A call to reconnect the tunnel while in error state can be used to attempt the recovery and exit the error state upon success. Note that actor decides the target state based on state prior to error state.
Diagram below explains state transitions. Note that colored arrows indicate entries and exits into/from error state based on prior state.
Inducing error state 🤰
Normally actor enters error state on its own if it had some issues configuring tunnel adapter or reading settings.
However, error state can also be induced by calling
Actor.setErrorState()
. This is particularly useful because packet tunnel provider initiates device checks. When device check points at critical error, such as invalid account or revoked device, this information has to be passed to the actor which otherwise would never be able to figure out on its own.Blocked state reason
When an error happens inside of actor, it needs to understand what went wrong. However it cannot potentially know all of the concrete errors that its dependencies may throw, since it relies on abstract implementations.
It needs some way to map errors to blocked state reason (
BlockedStateReason
) which can be used to take big picture decisions.A special type implementing
BlockedStateErrorMapperProtocol
is used for that purpose.Interruption
.connecting
,.reconnecting
,.error
states can be interrupted if the tunnel is requested to stop, which should segue actor towards.disconnected
state.Reacting to key rotation 🔑
The actor maintains a key policy (
KeyPolicy
) which describes what WG key to use for tunnel communication. The new key, when created and sent to our API, cannot be used immediately, since it takes time for it to propagate across all relays. So the actor has to use the prior key for a short period of time before switching to new key. Note that there is a grace period before the old key is "forgotten" by our relays for that specific purpose.When the main app rotates the key, it sends an IPC message (
TunnelProviderMessage.privateKeyRotation
) to the packet tunnel to notify it about what happened. In response, the actor caches the active WG key and changes key policy toKeyPolicy.usePrior
. Then it starts a task that waits for 120 seconds before forgetting the cached key and starting to use the new one, changing policy toKeyPolicy.useCurrent
.Starting the tunnel
When starting the tunnel, the system invokes
PacketTunnelProvider.startTunnel()
which in turn callsActor.start()
. Note thatstartTunnel()
is async and we shouldawait
in it until the tunnel is fully connected, because network extension shifts system VPN status to "connected" upon return.Actor.start()
solves that by automatically scheduling an async waiter that returns once the tunnel is connected. For safety reasons, the same waiter will return if the tunnel state changed to disconnected. Normally it means that a call toPacketTunnelProvider.stopTunnel()
was received before the tunnel was able to establish connection. In that case it's reasonable to drop everything and head for the exits.Stopping the tunnel
The system calls
PacketTunnelProvider.stopTunnel()
and gives network extension the opportunity to do clean up and shutdown. In turn provider callsActor.stop()
.It's important to note that
Actor.stop()
will cancel all of the preceding tasks and shutdown all facilities as soon as possible, ignoring, but logging any errors that may occur during that time.TaskQueue
Actors in Swift are reentrant.TaskQueue
provides a mechanism to guarantee that tasks execute in transactional fashion and on FIFO basis. It also provides a mechanism for establishing relationships between different tasks (seeTaskKind
) and mostly tailored for a packet tunnel actor but can easily be made more generalized.CommandChannel
In principle there are couple of issues we experience, when working with async flows and actors which channel attempts to solve directly or indirectly:
await
without fearing re-entrancy as commands are consumed usingfor-await
loop.async let
orTask
both seem to execute on a concurrent queue creating parallelism which introduces call ordering issues.Review suggestions
Start review from
PacketTunnelProvider.swift
, then walk down its dependencies. I think the most important is to look atPacketTunnelCore/Actor
, look atState
,Actor
,TaskQueue
. ThenAppMessageHandler
and then the rest.Everything under
MullvadVPN/
is mostly noise and small changes to make it compile. I left a lot of TODOs in there which I don't expect to fix in this PR but I think it's good that we have them remind us about themselves in Xcode.This change is